14.3 Entscheidungsbäume in der Praxis#

Hinweise zur Vorlesung Objektorientierte Programmierung im WiSe 2025/26

Dieses Vorlesungsskript wird gerade umgebaut.

Entscheidungsbäume bieten viele Vorteile, haben aber auch Nachteile, die wir in diesem Kapitel diskutieren werden. Darüber hinaus lernen wir Methoden kennen, bei Entscheidungsbäumen diese Nachteile zu reduzieren.

Lernziele#

Lernziele

  • Sie können in eigenen Worten erklären, was Overfitting (deutsch: Überanpassung) ist.

  • Sie wissen, was Underfitting bedeutet.

  • Sie wissen, dass Entscheidungsbäume eine Tendenz zu Overfitting haben und Maßnahmen zur Reduzierung von Overfitting ergriffen werden müssen.

  • Sie wissen, was Hyperparameter sind.

  • Sie kennen Hyperparameter der Entscheidungsbäume wie beispielsweise

    • maximale Baumtiefe,

    • minimale Anzahl an Datenpunkten in Knoten oder

    • minimale Anzahl an Datenpunkten in Blättern.

  • Sie können die Hyperparameter zum Prä-Pruning (deutsch: vorab Zurechtschneiden) geeignet wählen.

Die Tendenz von Entscheidungsbäumen zum Overfitting#

Entscheidungsbaummodelle bieten zahlreiche Vorteile. Ein wesentlicher Vorzug ist die Möglichkeit, den trainierten Entscheidungsbaum zu visualisieren, wodurch es leicht nachvollziehbar wird, welche Merkmale einen signifikanten Einfluss haben. Ein weiterer Vorteil ist ihre Effizienz bei heterogenen Daten; sowohl numerische als auch kategoriale Eigenschaften können problemlos verarbeitet werden. Entscheidungsbäume sind selbst bei unterschiedlichen Datenskalen robust und erfordern nur wenig Vorverarbeitung.

Trotz dieser Stärken besitzen Entscheidungsbäume eine Neigung zum Overfitting. Overfitting, auch als Überanpassung bekannt, beschreibt ein Problem im maschinellen Lernen, bei dem ein Modell die Trainingsdaten zu genau lernt. Das klingt zunächst gut, aber das Modell kann dadurch seine Fähigkeit verlieren, Vorhersagen für neue, unbekannte Daten zu treffen. Im Gegensatz dazu steht das Underfitting, das eine zu geringe Anpassung an die Daten bedeutet und ebenfalls unerwünscht ist.

Um uns das Problem des Overfittings zu veranschaulichen, betrachten wir erneut das Autohaus-Beispiel, aber diesmal mit mehr Autos. Wir lassen die Autos diesmal mit einer in Scikit-Learn eingebauten Funktion zur Generierung von künstlichen Daten erzeugen, der sogenannten make_moons-Funktion (siehe Dokumentation Scikit-Learn → make_moons) aus dem Module sklearn.datasets.

from sklearn.datasets import make_moons 

X_array, y_array = make_moons(noise = 0.5, n_samples=50, random_state=3)

Damit die künstlichen Daten besser zu dem Autohaus-Beispiel passen, transformieren wir sie und nutzen die Pandas-Datenstrukturen, um sie effizient zu verwalten.

import numpy as np
import pandas as pd

# Transformation der Merkmalswerte in einen positiven Bereich und 
# Umwandlung in eine Integer-Matrix
X_array = X_array + 1.2 * np.abs(np.min(X_array))
X_array = X_array + 1.2 * np.abs(np.min(X_array))
X_array[:,0] = np.ceil(X_array[:,0] * 30000)
X_array[:,1] = np.ceil(X_array[:,1] * 10000)
X = pd.DataFrame(X_array, columns=['Kilometerstand [km]', 'Preis [EUR]'], dtype=(int, int))

# Zuweisung von True/False basierend auf den Kategorien 1 bzw. 0
y_array = (y_array - 1.0) * (-1)
y = pd.Series(y_array, name='verkauft', dtype='bool')

Nach der Datenvorbereitung visualisieren wir diese:

import plotly.express as px

fig = px.scatter(x = X['Kilometerstand [km]'], y = X['Preis [EUR]'], color=y,
    title='Künstliche Daten Autohaus',
    labels={'x': 'Kilometerstand [km]', 'y': 'Preis [EUR]', 'color': 'verkauft'})
fig.show()

Das Training des Entscheidungsbaumes und dessen Visualisierung erledigt der folgende Code.

from sklearn.tree import DecisionTreeClassifier, plot_tree

modell = DecisionTreeClassifier(random_state=0)
modell.fit(X,y)

plot_tree(modell,
    feature_names=['Kilometerstand [km]', 'Preis [EUR]'],
    class_names=['nicht verkauft', 'verkauft']);
../_images/83c5c9957f08b343436f295280bf01a92f7df0e859c28194ba21ea23d07fef6f.png

Die Visualisierung offenbart zahlreiche Verzweigungen und eine schwer lesbare Beschriftung. Die Entscheidungsgrenzen, die im Folgenden mit DecisionBoundaryDisplay visualisiert werden, zeigen eine zu starke Anpassung an die Trainingsdaten.

import matplotlib.pyplot as plt
import matplotlib as mpl
from matplotlib.colors import ListedColormap
from sklearn.inspection import DecisionBoundaryDisplay

fig = DecisionBoundaryDisplay.from_estimator(modell, X, cmap=ListedColormap(['#EF553B33', '#636EFA33']), grid_resolution=1000)
fig.ax_.scatter(X['Kilometerstand [km]'], X['Preis [EUR]'], c=y, cmap=ListedColormap(['#EF553B', '#636EFA']))
fig.ax_.set_title('Entscheidungsgrenzen');
../_images/055645edfedb4b18b93e728352e695df45938609b78fd7343cb0d66d72fffeb2.png

Es ist fraglich, ob dieser Entscheidungsbaum nicht zu genau an die Trainingsdaten angepasst wurde. Der dünne blaue vertikale Streifen bei ungefähr 97000 km ist wahrscheinlich keine sinnvolle Entscheidung, sondern eher einem Ausreißer geschuldet (dem Auto mit einem Kilometerstand von 97098 km und einem Preis von 28229 EUR). Der Entscheidungsbaum hat sich zu stark an die Daten angepasst. Es ist wahrscheinlich, dass dieser Entscheidungsbaum für Autos mit einem Kilometerstand von ungefähr 97000 km falsche Prognosen treffen wird. Wenn wir mit den gleichen Daten erneut einen Entscheidungsbaum trainieren lassen und den Zufallszahlengenerator mit dem Zustand random_state=1 initialisieren, erhalten wir ein völlig anderes Ergebnis.

modell_alternative = DecisionTreeClassifier(random_state=1)
modell_alternative.fit(X,y)

fig = DecisionBoundaryDisplay.from_estimator(modell_alternative, X, cmap=ListedColormap(['#EF553B33', '#636EFA33']), grid_resolution=1000)
fig.ax_.scatter(X['Kilometerstand [km]'], X['Preis [EUR]'], c=y, cmap=ListedColormap(['#EF553B', '#636EFA']))
fig.ax_.set_title('Entscheidungsgrenzen des alternativen Modells');
../_images/7a3e5787f65321efc109f376608bbc03c0b507d021bf40532c6d71689e731ec5.png

Eine Möglichkeit, das Overfitting (Überanpassung) an die Daten zu bekämpfen, ist das Zurechtschneiden (Pruning) der Entscheidungsbäume. Eine andere ist, aus mehreren Entscheidungbäumen einen »durchschnittlichen« Entscheidungsbaum zu bilden. Dieses Verfahren heißt Zufallswald (Random Forest) und wird ausführlich in einem eigenen Kapitel behandelt werden. In diesem Kapitel betrachten wir nur das Zurechtschneiden der Entscheidungsbäume.

Zurechtschneiden von Entscheidungsbäumen#

Eine effektive Strategie zur Bekämpfung des Overfitting-Problems bei Entscheidungsbäumen ist das sogenannte Pruning, also das Beschneiden des Baumes. Pruning hilft, die Komplexität des Modells zu reduzieren, indem weniger relevante Entscheidungszweige nach bestimmten Kriterien entfernt werden. Im Kontext unseres Autohaus-Beispiels würde dies bedeuten, dass Entscheidungszweige, die beispielsweise aufgrund von Ausreißern entstanden sind, abgeschnitten werden. Dies könnte beispielsweise den zuvor erwähnten dünnen blauen Streifen bei einem Kilometerstand von ungefähr 97000 km betreffen, der wahrscheinlich durch einen Ausreißer entstanden ist. Durch das Entfernen solcher spezifischen Anpassungen kann der Entscheidungsbaum besser verallgemeinern und wird robuster gegenüber neuen, unbekannten Daten. Das Ergebnis ist ein Modell, das eine bessere Balance zwischen Anpassung an die Trainingsdaten und Generalisierungsfähigkeit aufweist.

Für Entscheidungsbäume gibt es prinzipiell zwei Methoden des Prunings: Prä-Pruning und Post-Pruning. Das Prä-Pruning findet vor dem Training des Entscheidungsbaumes statt, das Post-Pruning nach dem Training. Die beiden wichtigsten Prä-Pruning-Maßnahmen sind

  • die Begrenzung der maximalen Tiefe des Baumes und

  • die Forderung nach einer Mindestanzahl von Datenpunkten (entweder pro Knoten oder pro Blatt).

Beim Post-Pruning werden im Nachhinein Knoten mit wenig Informationen aus dem Entscheidungsbaum entfernt oder es werden Knoten zusammengelegt. Scikit-Learn hat nur Prä-Pruning implementiert, so dass wir hier nicht weiter auf Post-Pruning eingehen.

Prä-Pruning: Baumtiefe#

Wir schauen uns zunächst an, wie bei Scikit-Learn-Entscheidungsbäumen die maximale Tiefe festgelegt wird. Bisher haben wir das Modell ohne weitere Parameter initialisiert (einzige Ausnahme: wir haben ggf. den Zufallszahlengenerator aus didaktischen Gründen fixiert, damit die Ergebnisse vergleichbar sind). Nun verwenden wir bei der Initialisierung des DecisionTreeClassifiers das optionale Argument max_depth= und setzen es auf 1.

modell_tiefe1 = DecisionTreeClassifier(random_state=0, max_depth=1)
modell_tiefe1.fit(X,y)

plot_tree(modell_tiefe1,
    feature_names=['Kilometerstand [km]', 'Preis [EUR]'],
    class_names=['nicht verkauft', 'verkauft']);
../_images/698be1c56ecc69c661bbc583d8edf01b5757b85443069260429121f0d37f9cc8.png

Eine Tiefe von 1 bedeutet, dass nur noch eine einzige Entscheidungsfrage gestellt wird. Das reicht nicht mehr, um die Autos in reine Blätter zu sortieren. Im linken Blatt sind 13 nicht verkaufte Autos und 24 verkaufte Autos, weshalb diesem Blatt die Kategorie »verkauft« zugeordnet wird. Im rechten Blatt sind 12 nicht verkaufte Autos und ein verkauftes Auto, so dass dieses Blatt insgesamt als »nicht verkauft« gilt. Die Visualisierung der Entscheidungsgrenzen zeigt, um welche Autos es sich handelt.

fig = DecisionBoundaryDisplay.from_estimator(modell_tiefe1, X, cmap=ListedColormap(['#EF553B33', '#636EFA33']), grid_resolution=1000)
fig.ax_.scatter(X['Kilometerstand [km]'], X['Preis [EUR]'], c=y, cmap=ListedColormap(['#EF553B', '#636EFA']))
fig.ax_.set_title('Entscheidungsgrenzen');
../_images/b0349685d8afd214bfc85315892478634a8e8f46ab57cded097693e821314c6b.png

Insbesondere die Visualisierung der Entscheidungsgrenzen zeigt aber auch, dass dieser Entscheidungsbaum nicht besonders gut die Daten erklärt. Der Score ist mit

print(f'Score des Entscheidungsbaumes mit Tiefe 1: {modell_tiefe1.score(X,y)}')
Score des Entscheidungsbaumes mit Tiefe 1: 0.72

auch nicht so gut. Daher verwenden wir nun als maximale Tiefe des Entscheidungsbaumes einen Wert von 2.

modell_tiefe2 = DecisionTreeClassifier(random_state=0, max_depth=2)
modell_tiefe2.fit(X,y)

plot_tree(modell_tiefe2,
    feature_names=['Kilometerstand [km]', 'Preis [EUR]'],
    class_names=['nicht verkauft', 'verkauft']);

print(f'Score des Entscheidungsbaumes mit Tiefe 2: {modell_tiefe2.score(X,y)}')
Score des Entscheidungsbaumes mit Tiefe 2: 0.78
../_images/a5b3355bd7c04d17efe1723a17f652ae4b7aa8e4ee027be01c26bffffd0c2eaf.png

Mit einem Score von 0.78 ist der Entscheidungsbaum mit einer maximalen Tiefe von 2 zwar besser als der Baum mit einer maximalen Tiefe von 1, aber deutlich entfernt von dem Score 1.0 bei einer Baumtiefe von 7. Die Entscheidungsgrenzen sehen folgendermaßen aus:

fig = DecisionBoundaryDisplay.from_estimator(modell_tiefe2, X, cmap=ListedColormap(['#EF553B33', '#636EFA33']), grid_resolution=1000)
fig.ax_.scatter(X['Kilometerstand [km]'], X['Preis [EUR]'], c=y, cmap=ListedColormap(['#EF553B', '#636EFA']))
fig.ax_.set_title('Entscheidungsgrenzen');
../_images/80de7545f28c0b461b21aa88c1ba32a66bdae338bb357eacb624ec85109d8b8c.png

Was ist jetzt besser, eine maximale Tiefe von 1 oder 2? Oder doch 3 vielleicht? Die Einführung der maximalen Tiefe bietet den Vorteil, das Overfitting zu bekämpfen. Der Nachteil davon ist, dass wir jetzt einen neuen Parameter haben, der das Training und die Prognose des Modells bestimmt. Und für diesen Parameter muss ein passender Wert eingestellt werden. Solche Parameter nennt man Hyperparameter.

Was ist … ein Hyperparameter?

Ein Hyperparameter ist ein Parameter, der vor dem Training eines Modells festgelegt wird und nicht aus den Daten während des Trainings gelernt wird. Die Hyperparameter steuern den gesamten Lernprozess und haben einen wesentlichen Einfluss auf die Leistung des Modells.

Kommen wir nun zu einem anderen Hyperparameter der Entscheidungsbäume, der Mindestanzahl von Datenpunkten.

Prä-Pruning: Mindestanzahl Datenpunkte#

Genau wie der Hyperparameter zur Begrenzung der Baumtiefe wird die Mindestanzahl der Datenpunkte vorab bei der Initialisierung des Entscheidungsbaumes festgelegt. Scikit-Learn bietet wiederum zwei Möglichkeiten, über die minimale Anzahl von Datenpunkten den Entscheidungsbaum zurechtzuschneiden. Zum einen kann für die Knoten eine minimal erforderliche Anzahl von Datenpunkten festgelegt werden, ab der es erlaubt ist, durch Entscheidungsfragen weiter zu verzweigen. Zum anderen kann eine minimale Anzahl an Datenpunkten für jedes Blatt festgelegt werden, das am Ende der Verzweigungen erreicht werden muss.

Wir probieren beide Möglichkeiten aus und vergleichen die Ergebnisse miteinander. Die Option zur Einstellung der Mindestanzahl pro Knoten heißt min_samples_split und die Option zur Einstellung des Mindestanzahl Datenpunkte pro Blatt heißt min_samples_leaf. Beiden optionalen Argumenten kann entweder ein Integer übergeben werden oder ein Float. Wird ein Integer übergeben, so ist damit die tatsächliche minimale Anzahl an Datenpunkten gemeint. Ein Float wird als Bruch interpretiert und meint die relative Anzahl der Datenpunkte. Der Bruch wird mit der Gesamtzahl der Datenpunkte multipliziert und dann wird auf die nächste ganze Zahl aufgerundet.

Schauen wir uns beide Varianten an. Zunächst begrenzen wir die Knoten und fordern, dass sich in jedem Entscheidungsknoten mindestens sechs Datenpunkte befinden müssen.

modell_knotenbegrenzung = DecisionTreeClassifier(random_state=0, min_samples_split=6)
modell_knotenbegrenzung.fit(X,y)

plot_tree(modell_knotenbegrenzung,
    feature_names=['Kilometerstand [km]', 'Preis [EUR]'],
    class_names=['nicht verkauft', 'verkauft']);

print(f'Score des Entscheidungsbaumes mit Prä-Pruning Mindestanzahl Datenpunkte pro Knoten: {modell_knotenbegrenzung.score(X,y)}')
Score des Entscheidungsbaumes mit Prä-Pruning Mindestanzahl Datenpunkte pro Knoten: 0.92
../_images/ed806a7d3b7d6f6453edaefa9d4881a0136ddcbb244894333bc1709bdc0aa350.png

Der Score ist 0.92. Nun fordern wir, dass in jedem Blatt mindestens sechs Datenpunkte verbleiben müssen.

modell_blattbegrenzung = DecisionTreeClassifier(random_state=0, min_samples_leaf=6)
modell_blattbegrenzung.fit(X,y)

plot_tree(modell_blattbegrenzung,
    feature_names=['Kilometerstand [km]', 'Preis [EUR]'],
    class_names=['nicht verkauft', 'verkauft']);

print(f'Score des Entscheidungsbaumes mit Prä-Pruning Mindestanzahl Datenpunkte pro Knoten: {modell_blattbegrenzung.score(X,y)}')
Score des Entscheidungsbaumes mit Prä-Pruning Mindestanzahl Datenpunkte pro Knoten: 0.82
../_images/a0e62e86f59199ff04c524952b399d8be40e100cbea4dfd67f5a8a1e8b233255.png

In diesem Fall erhalten wir einen Entscheidungsbaum mit einem Score von 0.82. Was jetzt die bessere Wahl ist – Begrenzung der Baumtiefe oder Festlegung einer Mindestanzahl von Datenpunkten Knoten/Blatt – und vor allem welche Wert der Hyperparameter haben soll, muss gesondert untersucht werden.

Video “How to Implement Decision Trees in Python / Scikit-Learn” von Misra Turp

Zusammenfassung und Ausblick#

In diesem Kapitel haben wir die Tendenz der Entscheidungsbäume zum Overfitting diskutiert. Um dem Problem des Overfittings zu begegnen, bietet Scikit-Learn die Möglichkeit des Prä-Prunings. Durch die Begrenzung der maximalen Baumtiefe oder die Festlegung einer Mindestanzahl von Datenpunkten in Knoten oder Blättern kann Overfitting reduziert werden. Diese zusätzlichen Parameter des Entscheidungsbaum-Modells werden Hyperparameter genannt und müssen adjustiert werden. Eine weitere Alternative, das Overfitting von Entscheidungsbäumen zu minimieren, bieten die Random Forests, die wir in einem späteren Kapitel kennenlernen werden.